package implementation1.app.streams;

import exceptions.StreamEOFException;
import exceptions.StreamException;
import exceptions.StreamReadingException;
import implementation1.gui.ShellWindow;
import interfaces.app.IStream;
import interfaces.gui.IShellWindow;

import java.util.ResourceBundle;
import java.util.concurrent.Semaphore;

/**
 * Creates and manages stream which takes input from command line and passes them to process.
 * Created: 24.10.11
 *
 * @author Jan Svab
 */
public class StdStream implements IStream
{
    /**
     * Resource of messages.
     */
    private ResourceBundle bundle = ResourceBundle.getBundle("implementation1.res.messages");

    /**
     * reference to shell window, from which will be the stream reads commands from user
     */
    private IShellWindow sh;
    /**
     * a type of opening of the stream
     */
    private int openAttribute = -1;
    /**
     * TRUE - stream is open a ready to read, otherwise FALSE
     */
    private boolean isOpen;
    /**
     * TRUE - stream is exclusive, FALSE - stream isn't exclusive
     */
    private boolean isExclusive;

    private Semaphore mutex;

    private boolean notifyingProcesses;

    /**
     * Constructor with setting of exclusivity.
     *
     * @param sh          a shell window
     * @param isExclusive setting of exclusivity
     */
    public StdStream(IShellWindow sh, boolean isExclusive)
    {
        this.sh = sh;
        isOpen = false;
        this.isExclusive = isExclusive;
        mutex = new Semaphore(1);

        notifyingProcesses = false;
    }

    /**
     * Constructor with setting of exclusivity on FALSE.
     *
     * @param sh a shell window
     */
    public StdStream(IShellWindow sh)
    {
        this(sh, true);
    }

    /**
     * Return true if the stream is opened.
     *
     * @return true if the stream is opened; otherwise false
     */
    @Override
    public boolean isOpen()
    {
        return isOpen;
    }

    /**
     * Return true if the stream is opened exclusively.
     *
     * @return true if the stream is opened exclusively; otherwise false
     */
    @Override
    public boolean isExclusive()
    {
        return isExclusive;
    }

    /**
     * Open the stream, if no possible return false.
     *
     * @param accessAttribute access to the stream
     * @return true if opening was successful; otherwise false
     */
    @Override
    public synchronized boolean tryOpen(int accessAttribute)
    {
        if (mutex.availablePermits() == 0)
        {
            return false;
        }

        try
        {
            open(accessAttribute);
        } catch (StreamException e)
        {
            return false;
        }

        return true;
    }

    /**
     * Open the stream, if no possible then wait.
     *
     * @param accessAttribute access to the stream
     * @throws StreamException Thrown if occurs error during opening of the stream.
     */
    @Override
    public synchronized void open(int accessAttribute) throws StreamException
    {
        if (accessAttribute != AccessAttributes.READ && accessAttribute != AccessAttributes.WRITE)
        {
            throw new StreamException(bundle.getString("error.stream.openOnlyForReadOrWrite"));
        }

        if (isExclusive)
        {
            try
            {
                mutex.acquire();

                if (notifyingProcesses)
                {
                    throw new StreamException(bundle.getString("error.stream.openStream"));
                }
            } catch (InterruptedException e)
            {
                throw new StreamException(bundle.getString("error.stream.openStream"));
            }
        }

        openAttribute = accessAttribute;
        isOpen = true;
    }

    /**
     * Close the stream.
     *
     * @throws StreamException if stream can not be closed
     */
    @Override
    public synchronized void close() throws StreamException
    {
        if (isExclusive && isOpen)
        {
            mutex.release();
        }

        isOpen = false;
        openAttribute = -1;
    }

    /**
     * Closes shell window.
     */
    public void closeShellWindow()
    {
        sh.close();
    }

    /**
     * Read a line from the stream if can not read wait.
     *
     * @return the line from stream
     * @throws StreamException    Thrown if occurs error during reading.
     * @throws StreamEOFException Thrown if occurs EOF on input.
     */
    @Override
    public synchronized String readLine() throws StreamException, StreamEOFException
    {
        if (!isOpen || openAttribute != AccessAttributes.READ)
        {
            throw new StreamReadingException(bundle.getString("error.stream.closeForRead"));
        }
        if (!Thread.currentThread().getName().equals("shell"))
        {
            ((ShellWindow) sh).setPrompt(Thread.currentThread().getName());
        }
        String message = sh.readln(true);

        if (message == null)
        {
            throw new StreamEOFException();
        }
        return message;
    }

    /**
     * Write a line to the stream.
     *
     * @param line a line
     * @throws StreamException Thrown if occurs error during writing into stream.
     */
    @Override
    synchronized public void writeLine(String line) throws StreamException
    {
        if (!isOpen || openAttribute != AccessAttributes.WRITE)
        {
            throw new StreamReadingException(bundle.getString("error.stream.closeForWrite"));
        }
        sh.writeln(line);
    }

    /**
     * Notify all waiting process.
     */
    @Override
    synchronized public void notifyAllProcess()
    {
        this.notifyAll();

        notifyingProcesses = true;
        while (mutex.hasQueuedThreads())
        {
            mutex.release();
        }
        notifyingProcesses = false;
    }

    @Override
    synchronized public void setExclusive(boolean exclusive)
    {
        this.isExclusive = exclusive;
    }
}
